/**
 * @file 数学和算法辅助
 * @author DuskyHuang 圣子
*/

import { Camera, IVec2Like, IVec3Like, Vec3, Node } from "cc";
import { STAR_R0, INCLINE, DEG_PER_HOUR, ASPECT_RATIO, DEG_RIGHT } from "const";

/**
 * 3D时角坐标系转笛卡尔系
 * @param ra 时角（世界坐标+Z逆时针角）
 * @param ha 高度角
 * @param radius 极半径
 * @param out 输出坐标
*/
export function hour2cartesian<T extends IVec3Like>(ra: num.deg, ha: num.deg, radius: number = 1, out?: T): T {
    return polar2cartesian(ra * DEG_PER_HOUR, ha, radius, out);
}

/**
 * 3D极坐标系转笛卡尔系
 * @param aa 方位角（世界坐标+Z逆时针角）
 * @param ha 高度角
 * @param radius 极半径
 * @param out 输出坐标
*/
export function polar2cartesian<T extends IVec3Like>(aa: num.deg, ha: num.deg, radius: number = 1, out?: T): T {
    const ra = Math.parseRadian(aa), dec = Math.parseRadian(ha);
    const res = out ?? {} as T;
    res.y = Math.sin(dec) * radius;
    const rxz = Math.cos(dec) * radius;
    res.x = Math.sin(ra) * rxz;
    res.z = Math.cos(ra) * rxz;
    return res;
}

/**
 * 天球坐标系转黄道坐标系
 * @param point 天球坐标
 * @param out 输出坐标
 * @param angle 地轴倾斜角
*/
export function equator2ecliptic<T extends IVec3Like>(point: IVec3Like, out?: T, angle?: num.deg): T {
    const res = (out ?? new Vec3) as T;
    const rad = Math.parseRadian(angle ?? INCLINE);
    return Vec3.rotateZ(res, point, Vec3.ZERO, rad);
}

/**
 * 利用普森公式计算尺度
 * @param abs 恒星绝对星等
 * @return 3D尺度
 * @explain 不使用视星等的原因是恒星在3D场景中会被放置在绝对坐标上。
*/
export function absoluteToRadius(abs: number): number {
    return Math.pow(10, (-abs / 5)) * STAR_R0;
}

/**
 * 利用深空尺寸数据估算世界尺度
 * @param distance 天体距离
 * @param diameter 天体直径
 * @param appDimension 视场秒度
 * @return [3D尺度, 3D视角]
 * @explain 不使用直接视大小的原因是深空天体在3D场景中会被放置在绝对坐标上。
*/export function appearanceToRadiusFov(distance: kly, diameter: lightyear, appD: sec | [sec, sec], pow10: pow10): [number, sec] {
    const absFov = (diameter / distance / 1000) * (60 * 180 / Math.PI); // 绝对视角
    const [w, h] = notEmptyArr(appD) ? [Math.max(...appD), Math.min(...appD)] : [appD, appD];
    const appFov = Math.avg(w, w, h);
    const avgFov = Math.avg(absFov, appFov);
    const adjust = avgFov / absFov;
    const relative = diameter * Math.pow(10, pow10) * adjust;
    return [relative * AVG_SLOP + AVG_ADD, absFov * adjust];
}
const AVG_SLOP = 0.3;
const AVG_ADD = 0.01;

/**
 * 求三角形内任一点重心坐标
 * @param A 顶点A平面坐标
 * @param B 顶点B平面坐标
 * @param C 顶点C平面坐标
 * @param P 任一点平面坐标
 * @return 重心坐标
*/
export function barycentric(A: IVec2Like, B: IVec2Like, C: IVec2Like, P: IVec2Like): Record<'a' | 'b' | 'c', number> {
    const { x: xa, y: ya } = A, { x: xb, y: yb } = B, { x: xc, y: yc } = C, { x, y } = P;
    const a = (-(x - xb) * (yc - yb) + (y - yb) * (xc - xb)) / (-(xa - xb) * (yc - yb) + (ya - yb) * (xc - xb));
    const b = (-(x - xc) * (ya - yc) + (y - yc) * (xa - xc)) / (-(xb - xc) * (ya - yc) + (yb - yc) * (xa - xc));
    return { a, b, c: 1 - a - b, }
}

/**
 * 四棱锥形视锥裁剪
 * @param camera 相机节点
 * @param target 观测节点
 * @param allowance 容忍度
 * @return 是否被裁剪
 * @explain 当近裁面足够近，远裁面足够远，物体足够小时的高效率视锥体裁剪
*/
export function pyramidCulling(camera: Camera, target: Node, allowance: num.pct = 0): boolean {
    const { right, up, forward, worldPosition } = camera.node;
    Vec3.subtract(_v3a, target.worldPosition, worldPosition);
    if (Vec3.dot(_v3a, forward) < (-allowance)) return true;
    const verMax = camera.fov / 2 * (1 + allowance), horMax = verMax / ASPECT_RATIO * (1 + allowance);
    const model = _v3a.length();
    const hor = DEG_RIGHT - Math.parseDegree(Math.acos(Vec3.dot(_v3a, right) / model));
    const ver = DEG_RIGHT - Math.parseDegree(Math.acos(Vec3.dot(_v3a, up) / model));
    return Math.abs(ver) > verMax || Math.abs(hor) > horMax;
}
const _v3a = new Vec3;

/**
 * 双范围减弱
 * @param attenuation 渐进范围
 * @param value 当前值
 * @param useSqr 使用平方距离
 * @return 减弱到比例
*/
export function attenuate(value: number, attenuation: attenuation, useSqr: boolean = false): num.pct {
    const params = useSqr ? attenuation.map(v => v * v) : attenuation;
    const [inFrom, inTo, outFrom, outTo] = params;
    if (params.length === 2) {
        if (value <= inFrom) return 0;
        if (value >= inTo) return 1;
        return (value - inFrom) / (inTo - inFrom);
    } else if (params.length === 4) {
        if (value <= inFrom) return 0;
        if (value >= outTo) return 0;
        if (value < inTo) return (value - inFrom) / (inTo - inFrom);
        if (value > outFrom) return (outTo - value) / (outTo - outFrom);
        return 1;
    }
}

/**
 * 计算纹理集UV
 * @param idx id号码
 * @param span 占用宽度
 * @param CELL_NUM 纹理集细分尺度
 * @return uv tilling
*/
export function calcTiling(idx: num.idx, span: num.posInt, CELL_NUM: num.posInt): tiling;
export function calcTiling(idx: num.idx, GRIDS: { COL: num.posInt, ROW: num.posInt }): tiling;
export function calcTiling(idx: num.idx, arg1: number | { COL: number, ROW: number }, arg2?: number): tiling {
    if (isNum(arg1)) {
        const CELL = 256 / arg2;
        const xy = arg1 * CELL;
        const z = (idx % arg2) * CELL;
        const w = Math.floor(idx / arg2) * CELL;
        return [xy, xy, z, w];
    } else {
        const { COL, ROW } = arg1;
        const CELL_W = 256 / COL;
        const CELL_H = 256 / ROW;
        return [CELL_W, CELL_H, (idx % COL) * CELL_W, Math.floor(idx / COL) * CELL_H];
    }
}

/**
 * 计算球体所占屏幕比 
 * @param sphere 球体节点
 * @param viewer 观察相机
 * @return 百分比（0-100）
*/
export function calcSphereScreenPercent(sphere: Node, viewer: Camera): num.pct {
    if (sphere.activeInHierarchy) {
        const viewDist = Vec3.distance(sphere.worldPosition, viewer.node.worldPosition);
        return (sphere.worldScale.x * viewer.camera.matProj.m05) / (viewDist * 2.0) * 100;
    } else return 0;
}

/**
 * 光滑步进函数
 * @param t1 0阈值
 * @param t2 1阈值
 * @param x 步进参数
*/
export function smoothstep(t1: number, t2: number, x: number): num.pct {
    const v = Math.clamp01((x - t1) / (t2 - t1));
    return v * v * (3 - 2 * v);
}

/**
 * 计算天体的合适尽头偏移量 
 * @param diam 直径（百万km）
 * @param smallest 最小直径
 * @param biggest 最大直径
 * @param nearest 最近焦距
 * @param farthest 最远焦距
*/
export function calcLensBias(diam: mkm, smallest: mkm, biggest: mkm, nearest: mkm, farthest: mkm): pow10 {
    return Math.lerp(farthest, nearest, Math.clamp01((diam - smallest) / (biggest - smallest)));
}